import { BotClientService } from '@boticord/bot-client'
import { botsMeiliSettings, ErrorCodes, Limits } from '@boticord/consts'
import {
  Bot,
  CreateBotDto,
  makeMeiliBot,
  MeiliIndexedBot,
  MeiliIndexedComment,
  PendingBot,
  Report,
  ResourceComment,
  ResourceStatus,
  ResourceUp,
  UpdateBotDto,
  UpdateBotStatsDto,
} from '@boticord/resources-client'
import {
  BadRequestException,
  ForbiddenException,
  forwardRef,
  Inject,
  Injectable,
  InternalServerErrorException,
  NotFoundException,
  OnModuleInit,
} from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { InjectRepository } from '@nestjs/typeorm'
import MeiliSearch from 'meilisearch'
import { Repository } from 'typeorm'
import { ValidationUtil } from '../validation.util'
import { ResourcesService } from './resources.service'
import { Permission } from '@boticord/auth'
import { BitFieldUtil } from '@boticord/common'
import { QualityCheckMethods } from '@boticord/consts/quality-check-methods'

@Injectable()
export class BotsService implements OnModuleInit {
  private readonly meili: MeiliSearch

  constructor(
    @InjectRepository(Bot) private readonly bots: Repository<Bot>,
    private readonly bot: BotClientService,
    private readonly util: ValidationUtil,
    @Inject(forwardRef(() => ResourcesService)) private readonly resources: ResourcesService,
    @InjectRepository(ResourceComment) private readonly comments: Repository<ResourceComment>,
    @InjectRepository(ResourceUp) private readonly up: Repository<ResourceUp>,
    @InjectRepository(PendingBot) private readonly pendingBots: Repository<PendingBot>,
    @InjectRepository(Report) private readonly reports: Repository<Report>,
    private readonly config: ConfigService,
    private readonly validation: ValidationUtil,
  ) {
    this.meili = new MeiliSearch({
      // WMEILISEARCH_* is parsed by us to not conflict with meilisearch config which starts with MEILI_
      host: this.config.getOrThrow('MEILISEARCH_HOST'),
      apiKey: this.config.getOrThrow('MEILI_MASTER_KEY'),
    })
  }

  async onModuleInit() {
    await this.ensureSettings()
  }

  private get index() {
    return this.meili.index<MeiliIndexedBot>('bots')
  }

  private async ensureSettings() {
    await this.index.updateSettings(botsMeiliSettings)
  }

  // DO NOT EXPOSE TO PUBLIC
  private async get(id: string) {
    return await this.bots.findOne({ where: { id }, relations: { developers: true, comments: true } })
  }

  async publicGet(id: string, requester?: string, requesterPermissions?: number) {
    let bot = await this.bots.findOne({
      where: { id },
      relations: { developers: true },
    })

    if (!bot) {
      bot = await this.bots.findOne({
        where: { shortLink: id },
        relations: { developers: true },
      })

      if (!bot) {
        throw new NotFoundException({ code: ErrorCodes.UnknownBot })
      }
    }

    id = bot.id

    const upCount = await this.up.count({
      where: {
        resource: { id },
      },
    })

    const ups = requester ? await this.up.find({
      where: {
        resource: { id },
        user: { id: requester },
      },
    }) : undefined

    if (requester) {
      if (
        !bot.developers.find(d => d.id === requester) 
        && !(requesterPermissions && BitFieldUtil.has(requesterPermissions, Permission.ModerateBots))
      ) {
        bot.notify = {
          enabled: false,
        }
      }
    } else {
      bot.notify = {
        enabled: false,
      }
    }

    return { ...bot, ups, upCount }
  }

  async getPending(id: string) {
    const bot = await this.pendingBots.findOne({
      where: { id }, relations: { developers: true },
    })

    if (!bot) {
      throw new NotFoundException({ code: ErrorCodes.UnknownBot })
    }

    return bot
  }

  async getPendingBots() {
    return await this.pendingBots.find({ where: { status: ResourceStatus.Pending } })
  }

  async getBlockedBots() {
    return [
      ...await this.bots.find({ where: { status: ResourceStatus.Banned } }),
      ...await this.pendingBots.find({ where: { status: ResourceStatus.Banned } }),
    ]
  }

  async has(id: string): Promise<boolean> {
    return (await this.bots.count({ where: { id } })) > 0
  }

  async indexate(id: string) {
    const bot = await this.bots.findOne({ where: { id } })
    if (bot === null) {
      await this.index.deleteDocument(id)
      throw new NotFoundException({ code: ErrorCodes.UnknownBot })
    }

    if (bot.status !== ResourceStatus.Public) {
      await this.index.deleteDocument(id)
      return
    }

    const ratingsCount = bot.ratings.reduce((a, b) => a + b.count, 0)
    const rating =
      ((bot.ratings.reduce((a, b) => a + b.count * b.rating, 0) / ratingsCount) || 0.00)
        .toFixed(1)

    const totalUps = await this.up.count({ where: { resource: { id: bot.id } } })

    // console.log('meili bot', makeMeiliBot(bot, totalUps, parseFloat(rating)))
    await this.index.addDocuments([ makeMeiliBot(bot, totalUps, parseFloat(rating), ratingsCount) ])
  }

  async delete(id: string, isPending?: boolean) {
    const commentsIndex = this.meili.index<MeiliIndexedComment>('comments')
    const comments = await this.comments.find({ where: { resource: { id } } })
    await commentsIndex.deleteDocuments(comments.map(c => c.id))

    await this.comments.delete({ resource: { id } })
    await this.up.delete({ resource: { id } })
    await this.reports.delete({ resource: { id } })

    if (isPending) {
      const pendingBot = await this.pendingBots.findOne({ where: { id } })
      if (pendingBot === null) {
        throw new NotFoundException({ code: ErrorCodes.UnknownBot })
      }

      return await this.pendingBots.delete({ id: pendingBot.id })
    } else {
      const bot = await this.bots.findOne({ where: { id } })
      if (bot === null) {
        throw new NotFoundException({ code: ErrorCodes.UnknownBot })
      }

      await this.index.deleteDocument(bot.id)
      return await this.bots.delete({ id: bot.id })
    }
  }

  async getBotWebhook(id: string) {
    return this.get('id').then(e => e?.notify ?? null)
  }

  async modifyBot(id: string, userID: string, dto: UpdateBotDto, perms: number) {
    let bot = await this.bots.findOne({ where: { id } })
    let pendingBot

    if (bot === null) {
      pendingBot = await this.pendingBots.findOne({ where: { id: id } })
      if (pendingBot === null) {
        throw new NotFoundException({ code: ErrorCodes.UnknownBot })
      }
    }

    if (bot) {
      // remove ability to add premium from not-owner
      if (bot.owner !== userID && !BitFieldUtil.has(perms, Permission.ModerateBots)) {
        perms = 0
      }
      console.log('perms', perms, bot.owner, userID)
      bot = await this.modifyBotWithoutSaving(bot, dto, perms) as Bot
      const result = await this.bots.save(bot)
      await this.indexate(bot.id)
      return result
    } else {
      // remove ability to add premium from not-owner
      if (pendingBot.owner !== userID && !BitFieldUtil.has(perms, Permission.ModerateBots)) {
        perms = 0
      }

      pendingBot = await this.modifyBotWithoutSaving(pendingBot, dto, perms) as PendingBot
      return await this.pendingBots.save(pendingBot)
    }
  }

  async modifyPendingBot(id: string, userID: string, dto: UpdateBotDto, perms: number) {
    return this.modifyBot(id, userID, dto, perms)
  }

  async createBot(userID: string, dto: CreateBotDto, perms: number, ip: string) {
    const user = await this.bot.getUser(dto.id)
    if (!user) {
      throw new InternalServerErrorException({ code: ErrorCodes.ThirdPartyFail })
    }

    const realBot = await this.bots.findOne({ where: { id: dto.id } })
    if (realBot) throw new NotFoundException({ code: ErrorCodes.DuplicateBot })

    const pendingBot = await this.pendingBots.findOne({ where: { id: `${userID}+${dto.id}` } })
    if (pendingBot) throw new NotFoundException({ code: ErrorCodes.DuplicateBot })

    if (process.env.NODE_ENV !== 'development') {
      const secret = this.config.get('TURNSTILE_SECRET')
      if (!secret) {
        throw new InternalServerErrorException({ code: ErrorCodes.ServiceConfiguredIncorrectly })
      }

      const response = await this.validation.validateTurnstile(secret, dto.token, ip)
      if (!response.success) {
        throw new ForbiddenException({ code: ErrorCodes.AutomatedRequestsNotAllowed })
      }
    }

    let bot = new PendingBot()

    switch (dto.qualityCheckMethod) {
      case QualityCheckMethods.Manual:
        bot.systemApproved = false
        break
      default: {
        const result = await this.checkMonitoringBot(dto.id, dto.qualityCheckMethod)
        if (!result.ok) throw new InternalServerErrorException({ code: ErrorCodes.ThirdPartyMonitoringFail })
        if (!result.approved) throw new BadRequestException({ code: ErrorCodes.ThirdPartyMonitoringNotApproved })
        bot.systemApproved = true
      }
    }

    bot.owner = userID
    bot = await this.modifyBotWithoutSaving(bot, dto, perms) as PendingBot
    bot.botID = dto.id
    bot.name = user.username
    bot.avatar = user.avatar
    bot.discriminator = user.discriminator
    bot.ratings = Array.from({ length: Limits.MaxRating }, (_, i) => ({ count: 0, rating: i + 1 }))
    bot.id = `${userID}+${dto.id}`

    return await this.pendingBots.save(bot)
  }

  async modifyStats(id: string, dto: UpdateBotStatsDto) {
    const bot = await this.bots.findOne({ where: { id } })
    if (bot === null) {
      throw new NotFoundException({ code: ErrorCodes.UnknownBot })
    }

    // @ts-ignore
    bot.members = dto.members ?? null
    // @ts-ignore
    bot.guilds = dto.guilds ?? null
    // @ts-ignore
    bot.shards = dto.shards ?? null

    const result = await this.bots.save(bot)

    await this.indexate(result.id)

    return result
  }

  async changeStatus(id: string, status: ResourceStatus) {
    const bot = await this.get(id)
    if (bot === null) {
      throw new NotFoundException({ code: ErrorCodes.UnknownBot })
    }

    bot.status = status
    const result = await this.bots.save(bot)

    await this.indexate(result.id)

    return result
  }

  async changePendingBotStatus(id: string, status: ResourceStatus.Pending | ResourceStatus.Banned) {
    const bot = await this.pendingBots.findOne({ where: { id } })
    if (bot === null) {
      throw new NotFoundException({ code: ErrorCodes.UnknownBot })
    }

    bot.status = status
    return await this.pendingBots.save(bot)
  }

  async approveBot(id: string) {
    const pendingBot = await this.pendingBots.findOne({
      where: { id },
      relations: { developers: true },
    })
    if (pendingBot === null) {
      throw new NotFoundException({ code: ErrorCodes.UnknownBot })
    }

    if (pendingBot.status !== ResourceStatus.Pending) {
      throw new BadRequestException({ code: ErrorCodes.BlockedBot })
    }

    let bot = new Bot()
    bot.owner = pendingBot.owner
    bot = await this.modifyBotWithoutSaving(bot, {
      ...pendingBot, developers: pendingBot.developers.map(u => u.id),
    }, 0) as Bot
    bot.id = pendingBot.botID
    bot.name = pendingBot.name
    bot.avatar = pendingBot.avatar
    bot.discriminator = pendingBot.discriminator
    bot.ratings = pendingBot.ratings
    bot.createdDate = pendingBot.createdDate
    bot.status = ResourceStatus.Public

    await this.bots.save(bot)
    await this.pendingBots.delete({ id })
    await this.indexate(bot.id)

    return bot
  }

  async fetchBotApp(id: string) {
    return this.resources.fetchApplicationData(id)
  }

  async checkMonitoringBot(id: string, method: QualityCheckMethods) {
    const result = await this.resources.checkMonitoringBot(id, method)
      .catch(() => ({ ok: false, result: { approved: false } }))

    return { ok: result.ok, approved: result.result.approved }
  }

  private async modifyBotWithoutSaving(bot: Bot | PendingBot, dto: UpdateBotDto, perms: number) {
    if (dto.shortLink) {
      bot.shortLink = await this.util.checkShortLink(bot.id, Bot, dto.shortLink)
    }

    const devs = await this.util.resolveUsers(dto.developers)
    console.log('devs', devs)
    if (!devs.some(dev => dev.id === bot.owner)) {
      throw new BadRequestException({ code: ErrorCodes.CannotDeleteOwner })
    }

    bot.developers = devs
    bot.prefix = dto.prefix
    bot.inviteLink = dto.inviteLink
    bot.description = dto.description
    bot.shortDescription = dto.shortDescription
    bot.tags = dto.tags
    bot.supportServerInviteCode = dto.supportServerInviteCode
    bot.library = dto.library
    bot.website = dto.website
    bot.standardBannerID = dto.standardBannerID ?? 0
    bot.notify = {
      enabled: !!dto.notify?.enabled,
    }

    if (BitFieldUtil.has(perms, Permission.Premium)) {
      bot.premium = {
        active: !!dto.premium?.active,
        autoFetch: !!dto.premium?.autoFetch,
        splashURL: dto.premium?.splashURL,
        bannerURL: dto.premium?.bannerURL,
      }
    }

    return bot
  }
}
